#!/usr/bin/env python3

"""
Author: Yanrui Hu
Date: 2023/4/12
Description: 包含了对于文件系统的操作方法
Keywords: Ubuntu, Container, Docker, 毕设, 文件系统操作
"""
import os
import logging
import subprocess
import stat

REPO_PATH = os.path.dirname(os.path.abspath(__file__))

logfile = f'{REPO_PATH}/.database/last-oper.log'

# ------------------------------------------------------------
# 配置日志
# ------------------------------------------------------------
# 创建一个 Logger 实例
record_logger = logging.getLogger('record')
record_logger.setLevel(logging.INFO)

# 创建一个 FileHandler 实例
file_handler = logging.FileHandler(logfile)
file_handler.setLevel(logging.INFO)

# 创建标准输出handler, 输出ERROR级别以上的日志
console_handler = logging.StreamHandler()
console_handler.setLevel(logging.ERROR)


# 设置日志消息格式
formatter = logging.Formatter("%(asctime)s - %(message)s")
file_handler.setFormatter(formatter)

console_formatter = logging.Formatter('%(levelname)s - %(message)s')
console_handler.setFormatter(console_formatter)


# 添加 handler 到 Logger 实例
record_logger.addHandler(file_handler)
record_logger.addHandler(console_handler)


# 创建一个 Logger 实例
running_logger2 = logging.getLogger('running2')
running_logger2.setLevel(logging.INFO)

# 创建一个 FileHandler 实例
file_handler2 = logging.FileHandler(f'{REPO_PATH}/running.log')
file_handler2.setLevel(logging.INFO)

# 设置日志消息格式
formatter = logging.Formatter(
    '%(asctime)s - %(name)s - %(levelname)s - %(filename)s:%(lineno)d %(funcName)s - %(message)s'
)
file_handler2.setFormatter(formatter)

# 添加 handler 到 Logger 实例
running_logger2.addHandler(file_handler2)
running_logger2.addHandler(console_handler)


# ----------------------------------------------------
# Follow are some tools.
# 他们可以在执行对应的操作时，打印出对应的操作信息，同时输出日志
# ----------------------------------------------------
def remove_dir(dir_path, quiet=False):
    """移除一个文件夹，之所以单独抽出来，是因为需要做一些日志处理"""
    running_logger2.info(f"Remove dir: {dir_path}")
    if not quiet:
        print("Remove dir:", dir_path)
    # os.system(f"rm -rv {dir_path}")
    # record_logger.info(f"Remove dir: {dir_path}")
    cmd = f"rm -rv {dir_path}"
    res = subprocess.run(cmd, shell=True, capture_output=True)
    if res.returncode != 0:
        running_logger2.error(f"When trying to remove dir: {dir_path}")
        running_logger2.error(res.stderr.decode("utf-8").strip())
        exit(res.returncode)
    outs = res.stdout.decode("utf-8").strip().split("\n")
    for out in outs:
        if out != "":
            record_logger.info(f"Remove dir: {out}")


def remove_file(file_path, quiet=False):
    """移除一个文件，之所以单独抽出来，是因为需要做一些日志处理"""
    running_logger2.info(f"Remove file: {file_path}")
    if not quiet:
        print("Remove file:", file_path)
    # os.system(f"rm -v {file_path}")
    # record_logger.info(f"Remove file: {file_path}")
    cmd = f"rm -v {file_path}"
    res = subprocess.run(cmd, shell=True, capture_output=True)
    if res.returncode != 0:
        running_logger2.error(f"When trying to remove file: {file_path}")
        running_logger2.error(res.stderr.decode("utf-8").strip())
        exit(res.returncode)
    output = res.stdout.decode("utf-8").strip()
    record_logger.info(f"Remove file: {output}")


def move_file(src, dest, quiet=False):
    """移动文件"""
    running_logger2.info(f"Move file: {src} -> {dest}")
    if not quiet:
        print("Move file:", src, "->", dest)
    # os.system(f"mv -v {src} {dest}")
    # record_logger.info(f"Move file: {src} -> {dest}")
    cmd = f"mv -v {src} {dest}"
    res = subprocess.run(cmd, shell=True, capture_output=True)
    if res.returncode != 0:
        running_logger2.error(f"When trying to move file: {src} -> {dest}")
        running_logger2.error(res.stderr.decode("utf-8").strip())
        exit(res.returncode)
    output = res.stdout.decode("utf-8").strip()
    record_logger.info(f"Move file: {output}")


def copy_file(src, dest, quiet=False):
    """复制文件"""
    running_logger2.info(f"Copy file: {src} -> {dest}")
    if not quiet:
        print("Copy file:", src, "->", dest)
    # os.system(f"cp --archive -v --backup=t {src} {dest}")
    # running_logger.info(f"Copy file: {src} -> {dest}")
    cmd = f"cp --archive -v --backup=t {src} {dest}"
    res = subprocess.run(cmd, shell=True, capture_output=True)
    if res.returncode != 0:
        running_logger2.error(f"When trying to copy file: {src} -> {dest}")
        running_logger2.error(res.stderr.decode("utf-8").strip())
        exit(res.returncode)
    output = res.stdout.decode("utf-8").strip()
    record_logger.info(f"Copy file: {output}")


def make_dir(dir, quiet=False):
    """创建文件夹
    -p, --parents     no error if existing, make parent directories as needed
    -v, --verbose     print a message for each created directory"""
    running_logger2.info(f"Make dir: {dir}")
    if not quiet:
        print("Make dir:", dir)
    # os.system(f"mkdir -vp {dir}")
    # running_logger.info(f"Make dir: {dir}")
    cmd = f"mkdir -vp {dir}"
    res = subprocess.run(cmd, shell=True, capture_output=True)
    if res.returncode != 0:
        running_logger2.error(f"When trying to make dir: {dir}")
        running_logger2.error(res.stderr.decode("utf-8").strip())
        exit(res.returncode)
    outs = res.stdout.decode("utf-8").strip().split("\n")
    for out in outs:
        if out != "":
            record_logger.info(f"Make dir: {out}")


def create_symlink(link_target: str, link_name: str, quiet=False):
    """创建软链接"""
    running_logger2.info(f"Create symlink: {link_name} -> {link_target}")
    if not quiet:
        print("Create symlink:", link_name, "->", link_target)
    cmd = f"ln -sv {link_target} {link_name}"
    res = subprocess.run(cmd, shell=True, capture_output=True)
    if res.returncode != 0:
        running_logger2.error(f"When trying to create symlink: {link_name} -> {link_target}")
        running_logger2.error(res.stderr.decode("utf-8").strip())
        exit(res.returncode)
    output = res.stdout.decode("utf-8").strip()
    record_logger.info(f'Create symlink: {output}')


def remove_symlink(link_name: str, quiet=False):
    """移除软链接"""
    running_logger2.info(f"Remove symlink: {link_name}")
    old_link_target = os.readlink(link_name)
    if not quiet:
        print("Remove symlink:", link_name)
    cmd = f"rm -v {link_name}"
    res = subprocess.run(cmd, shell=True, capture_output=True)
    if res.returncode != 0:
        running_logger2.error(f"When trying to remove symlink: {link_name}")
        running_logger2.error(res.stderr.decode("utf-8").strip())
        exit(res.returncode)
    output = res.stdout.decode("utf-8").strip()
    record_logger.info(f'Remove symlink: {output} -> {old_link_target}')


def update_symlink(new_link_target: str, link_name: str, quiet=False):
    """更新软链接"""
    running_logger2.info(f"Update symlink: {link_name} -> {new_link_target}")
    old_link_target = os.readlink(link_name)
    if not quiet:
        print("Update symlink:", link_name, "->", new_link_target)
    # os.system(f"ln -sf {link_target} {link_name}")
    # running_logger.info(f"Update symlink: {link_target} -> {link_name}")
    cmd = f"ln -sfv {new_link_target} {link_name}"
    res = subprocess.run(cmd, shell=True, capture_output=True)
    if res.returncode != 0:
        running_logger2.error(
            f"When trying to update symlink: {link_name} -> {new_link_target}"
        )
        running_logger2.error(res.stderr.decode("utf-8").strip())
        exit(res.returncode)

    output = res.stdout.decode("utf-8").strip()
    record_logger.info(
        f'Update symlink: {output} (old link target: \'{old_link_target}\')'
    )


# ----------------------------------------------------
# Follow are about copy files kit.
# 他们是对于文件复制的一些工具, 包含了递归复制, 多目录复制等
# ----------------------------------------------------
def copy_multi(diff_dirs, dest_dir="/", quiet=False):
    dest_dir = os.path.expanduser(dest_dir)
    running_logger2.info(f"Copy multi dirs: {diff_dirs} -> {dest_dir}")
    if not quiet:
        print("\n\nIn copy_multi, diff_dirs:\n", diff_dirs)

    for diff_dir in diff_dirs:
        copy_recur_0(diff_dir, dest_dir, quiet)


def copy_recur_0(diff_dir: str, dest_dir: str = "/", quiet=False):
    """copy diff folder to local recursively
    递归复制目录,
    解决了 copy_recur_1 中冗余的问题
    目前只是对文件内容进行比对, TODO: 比对文件属性(mode, timestamp)
    保持src和dst一致
    """
    running_logger2.info(f"Copy recur 0: {diff_dir} -> {dest_dir}")
    m = len(diff_dir)  # 获取 diff_dir 的 长度, 用于字符串切片
    """eg:
    root = "/home/hyr/.local/share/containers/storage/overlay/1e82d30604c34f5930a1724a734686edf314a89d5920760166ae66125f412133/diff/X/XX"
    root[m:] => "/X/XX"

    root = "/home/hyr/.local/share/containers/storage/overlay/1e82d30604c34f5930a1724a734686edf314a89d5920760166ae66125f412133/diff/root/Z/ZZ/ZZZ"
    root[m:] => "/root/Z/ZZ/ZZZ"
    """
    if not quiet:
        print("\tIn copy_recur_0: 即将遍历diff目录树进行复制...")
        print(f"\tDiff_dir: {diff_dir}")
        print(f"\tDest_dir: {dest_dir}")
    # want_continue()
    for root, dirs, files in os.walk(diff_dir):  # Top-Down遍历目录树, 返回生成器
        running_logger2.info(f"root: {root}, dest_dir: {dest_dir}")
        dest = os.path.join(dest_dir, root[m + 1 :])  # 随着root的更新而更新

        # TODO: Get the permission mode (mask) of root directory & Apply it to the dest directory
        if not os.path.exists(dest):
            running_logger2.info(f"Create dir: {dest}")
            make_dir(dest, quiet)

        for file in files:  # For each file in "root" directory
            src = os.path.join(root, file)
            dst = os.path.join(dest, file)  # 利用之前的 m 进行目录的裁剪
            """ TODO: 当遇到同名的字符文件时, 按道理需要删除本地的文件, 因为这是相当于使用了mv或者rm的效果.
            但是，仅仅从最终的效果上看,我们无法得知一个文件的消失究竟是因为何种原因。故而无法使用mv进行高效的操作.
            此后或许会遇到因为使用mv而得到的重命名文件,但是我们仍然无法得知此文件是否是重命名得来的, 或者是其他尚未知晓的原因.
            总之,我们最好还是使用cp进行复制, 而不是使用mv进行移动. """
            file_stats = os.stat(src, follow_symlinks=False)
            # 之前很多问题都出在这个 follow_symlinks 上, 以前没有加上这个参数，导致软连接判断时总是出错
            # (容器内的软连接文件所指的文件在宿主机上几乎百分百无法找到)
            if stat.S_ISCHR(file_stats.st_mode) and file_stats.st_size == 0: # 判断是否是占用0空间的字符设备文件
                continue

            """ Finished: 遇到软链接文件(Symbolic link)时, 需要模仿软链接创建一个新的软链接文件,
            而不是复制, 并且, 软链接文件无法直接复制, 也无法使用diff比对
            """
            # 判断一个文件是否是软链接文件，如果是软链接文件，读取软链接的指向，然后创建一个新的软链接文件
            if os.path.islink(src) and not os.path.exists(dst):
                link_target = os.readlink(src)  # link_target 是字符串
                create_symlink(link_target, dst, quiet)
                continue
            elif os.path.islink(src) and os.path.exists(dst):
                new_link_target = os.readlink(src)
                old_link_target = os.readlink(dst)
                if new_link_target != old_link_target:
                    update_symlink(new_link_target, dst, quiet)
                else:
                    if not quiet:
                        print(f"SKIP SAME SYMLINK: ...{src[-20:]} <=> {dst}")

                continue
            # 判断 dst 是否存在
            if not os.path.exists(dst):
                copy_file(src, dst, quiet)
                continue

            # 判断两个文件src和dst是否相同
            res = subprocess.run(f"diff {src} {dst}", shell=True, capture_output=True)

            is_same = res.returncode == 0
            is_diff = res.returncode == 1
            is_error = res.returncode == 2
            if is_error:
                running_logger2.error(f"When trying to diff file: {src} <=> {dst}")
                running_logger2.error(res.stderr.decode("utf-8").strip())
                exit(res.returncode)
            if is_same and not quiet:
                print(f'SKIP FILE OF SAME CONTENT: ...{src[-20:]} <=> {dst}')
            elif is_diff:
                if not quiet:
                    print(f"COPY WITH BACKUP ...{src[-20:]} -> {dst}")
                copy_file(src, dst, quiet)

            # os.system(f"cp -a {src} {dst}") # 优先推荐！！More recommended! 复制软链接时仍然是软链接, 而且属性信息和时间信息都保留
            # os.system(f"cp   {src} {dst}")


def copy_recur_1(src, dest="/", quiet=False):
    """递归复制目录
    相比于前一个, 具有一些不足, 如「冗余备份」问题
    当多次运行脚本之后, 即便dest的文件没有更改, 也会发生备份。
    这是由于 cp --backup=t 没有办法针对相同的文件进行跳过处理导致的
    解决办法：在复制的同时, 判断文件的属性和内容是否一致, 如果一致, 则跳过此文件(不复制)
    这样也就可以减少不必要的冗余了
    """
    running_logger2.info(f"Copy recur 1: {src} -> {dest}")
    if not quiet:
        print("\n\nIn copy_recur_1, src:\n", src)

    make_dir(dest, quiet)  # 确保目标目录存在

    if not quiet:
        print(f"即将从 '{src}' 到 '{dest}' 递归复制")
    # want_continue()
    exit_code = os.system(f"cp -a -v --backup=t {src}/* {dest}") >> 8  #
    assert exit_code == 0, print(f"exit_code: {exit_code} 递归复制目录失败! 请手动排查错误")

    if not quiet:
        print(f"从{src}到 {dest}递归复制成功")
